Skip to content

Conversation

@lvalentine6
Copy link
Member

@lvalentine6 lvalentine6 commented Jul 16, 2025

✨ 개요

  • 스토리 등록 기능을 구현했습니다.
  • 기존 ERD 설계와 달리, Story 엔티티에서 Store와의 연관을 제거하고 스토어 정보를 비정규화하여 직접 저장하도록 변경했습니다.
  • 현재는 모든 클라이언트 요청마다 카카오 API를 두 번 호출하고 있어, 스토리 하나를 등록할 때 API 호출이 중복되는 상황입니다. 이는 추후 개선할 예정입니다.
  • GlobalExceptionHandler에 기본 로그 출력을 추가했습니다. 로깅 전략이 정해지기 전까지는 해당 방식으로 사용하면 될 것 같습니다.
  • ImageService의 도메인 관련 로직은 enum으로 명시화하여 가독성과 안정성을 높였습니다.
  • 곧 스토리 목록 조회 및 상세 조회 기능도 PR이 올라갈 예정입니다.

🧾 관련 이슈

#65

🔍 참고 사항 (선택)

Summary by CodeRabbit

Summary by CodeRabbit

  • 신규 기능

    • 스토리 등록 기능이 추가되어 사용자가 매장에 대한 스토리와 이미지를 등록할 수 있습니다.
    • 스토리 등록 시 매장 정보 검색 및 필터링, 이미지 업로드가 지원됩니다.
  • 버그 수정

    • 예외 발생 시 에러 코드 및 메시지가 로그에 기록되도록 개선되었습니다.
  • 문서화

    • 스토리 등록 API에 대한 테스트 및 문서가 추가되었습니다.
  • 테스트

    • 스토리 등록, 도메인 유효성, 서비스 동작에 대한 단위 및 통합 테스트가 추가되었습니다.
  • 기타

    • 스토리 관련 데이터베이스 테이블 및 에러 코드가 추가되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Jul 16, 2025

"""

Walkthrough

스토리(Story) 기능이 도메인, 서비스, 컨트롤러, DTO, 예외, 리포지토리, 테스트, 마이그레이션 등 전반에 걸쳐 새롭게 도입되었습니다. 사용자는 매장 검색 결과에서 매장을 선택해 스토리를 등록할 수 있으며, 이미지 업로드 및 다양한 유효성 검증이 적용됩니다.

Changes

파일/경로 그룹 변경 요약
src/main/java/eatda/controller/story/FilteredSearchResult.java, StoryRegisterRequest.java, StoryResponse.java, StoriesResponse.java 스토리 관련 DTO 및 응답 레코드 신설
src/main/java/eatda/controller/story/StoryController.java 스토리 등록용 REST 컨트롤러 추가 (POST /api/stories 엔드포인트)
src/main/java/eatda/domain/story/Story.java JPA 엔티티 Story 신설, 생성자 유효성 검증 포함
src/main/java/eatda/exception/BusinessErrorCode.java 스토리 및 매장 관련 비즈니스 에러코드 대거 추가
src/main/java/eatda/exception/GlobalExceptionHandler.java 비즈니스 및 일반 예외 발생 시 로그 기록 추가
src/main/java/eatda/repository/story/StoryRepository.java Story 엔티티용 리포지토리 인터페이스 신설 및 getById 기본 메서드 제공
src/main/java/eatda/service/common/ImageDomain.java 이미지 도메인 구분용 enum 신설
src/main/java/eatda/service/common/ImageService.java upload 메서드 ImageDomain enum 타입 적용, 기본 확장자 상수화 등
src/main/java/eatda/service/store/StoreService.java 검색 결과 리스트 반환용 searchStoreResults(String) 메서드 추가
src/main/java/eatda/service/story/StoryService.java 스토리 등록 서비스 신설, 매장 검색·필터링·이미지 업로드·스토리 생성 및 저장 로직 구현
src/main/resources/db/migration/V3__add_story_table.sql story 테이블 생성 마이그레이션 추가
src/test/java/eatda/controller/BaseControllerTest.java, document/BaseDocumentTest.java, service/BaseServiceTest.java 스토리 및 이미지 서비스 모킹 필드 추가
src/test/java/eatda/controller/story/StoryControllerTest.java StoryController 등록 API 단위 테스트 신설
src/test/java/eatda/document/Tag.java STORY_API 태그 추가
src/test/java/eatda/document/story/StoryDocumentTest.java 스토리 등록 API 문서화 및 성공/실패 케이스 문서 테스트 신설
src/test/java/eatda/domain/story/StoryTest.java Story 엔티티 생성 및 유효성 검증 테스트 신설
src/test/java/eatda/service/common/ImageServiceTest.java 이미지 업로드 도메인 파라미터화 및 관련 테스트 보강
src/test/java/eatda/service/story/StoryServiceTest.java StoryService 스토리 등록 성공/실패 테스트 신설

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant StoryController
  participant StoryService
  participant StoreService
  participant ImageService
  participant StoryRepository
  participant MemberRepository

  Client->>StoryController: POST /api/stories (multipart/form-data)
  StoryController->>StoryService: registerStory(request, image, memberId)
  StoryService->>StoreService: searchStoreResults(query)
  StoreService-->>StoryService: List<StoreSearchResult>
  StoryService->>ImageService: upload(image, ImageDomain.STORY)
  ImageService-->>StoryService: imageKey
  StoryService->>MemberRepository: findById(memberId)
  MemberRepository-->>StoryService: Member
  StoryService->>StoryRepository: save(Story)
  StoryRepository-->>StoryService: Story
  StoryService-->>StoryController: (void)
  StoryController-->>Client: 201 Created
Loading

Possibly related PRs

Suggested labels

released on @beta

Suggested reviewers

  • leegwichan

Poem

🐇
새로운 스토리, 매장과 함께
이미지를 올리고 추억을 담네
예외도 꼼꼼히, 검증도 빼곡히
테스트와 문서로 튼튼하게
토끼는 깡충, 코드를 축하해!
🥕✨
"""


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f716899 and e527cf5.

📒 Files selected for processing (2)
  • src/main/java/eatda/domain/story/Story.java (1 hunks)
  • src/test/java/eatda/service/BaseServiceTest.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/eatda/domain/story/Story.java
  • src/test/java/eatda/service/BaseServiceTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (8)
src/main/java/eatda/service/store/StoreService.java (1)

23-26: 코드 중복 리팩토링 제안

새로운 메서드 searchStoreResults가 기존 searchStores 메서드와 유사한 로직을 가지고 있습니다. 공통 로직을 추출하여 중복을 제거할 수 있습니다.

다음과 같이 리팩토링할 수 있습니다:

    public StoreSearchResponses searchStores(String query) {
-        List<StoreSearchResult> searchResults = mapClient.searchShops(query);
-        List<StoreSearchResult> filteredResults = storeSearchFilter.filterSearchedStores(searchResults);
-        return StoreSearchResponses.from(filteredResults);
+        return StoreSearchResponses.from(searchStoreResults(query));
    }

    public List<StoreSearchResult> searchStoreResults(String query) {
        List<StoreSearchResult> searchResults = mapClient.searchShops(query);
        return storeSearchFilter.filterSearchedStores(searchResults);
    }
src/test/java/eatda/controller/story/StoryControllerTest.java (1)

29-30: 중첩 클래스명 수정 필요

중첩 클래스명이 SearchStores로 되어 있지만, 실제로는 스토리 등록을 테스트하고 있습니다. 클래스명을 RegisterStory로 변경하는 것이 적절합니다.

    @Nested
-    class SearchStores {
+    class RegisterStory {
src/main/java/eatda/service/story/StoryService.java (2)

50-61: 매장 검색 성능 최적화를 고려해보세요.

현재 구현은 모든 검색 결과를 가져온 후 클라이언트에서 필터링하고 있습니다. 검색 결과가 많을 경우 성능상 비효율적일 수 있습니다.

다음과 같은 개선을 제안합니다:

  • StoreService에서 kakaoId로 직접 매장을 조회하는 메서드 추가
  • 또는 검색 단계에서 특정 매장 ID로 필터링하는 기능 추가
// 제안: StoreService에 새로운 메서드 추가
public Optional<FilteredSearchResult> findStoreByKakaoId(String query, String kakaoId) {
    // 검색 결과에서 직접 특정 매장만 반환
}

50-50: 메서드명이 의도를 명확히 나타내지 않습니다.

filteredSearchResponse 메서드명이 단순히 "필터링된 응답"을 의미하여 구체적인 기능을 파악하기 어렵습니다.

다음과 같은 명명을 제안합니다:

private FilteredSearchResult findStoreByKakaoId(List<StoreSearchResult> responses, String storeKakaoId)
src/main/java/eatda/controller/story/StoryController.java (1)

19-19: 기본 경로 매핑 고려해보세요.

컨트롤러 클래스 레벨에서 @RequestMapping("/api/stories")를 사용하여 경로를 정리하고, 메서드에서는 @PostMapping만 사용하는 것을 고려해보세요.

@RestController
@RequestMapping("/api/stories")
public class StoryController {
    
    @PostMapping
    public ResponseEntity<Void> registerStory(...)
}
src/main/resources/db/migration/V3__add_story_table.sql (1)

1-13: 데이터베이스 스키마 설계가 적절합니다.

테이블 구조와 제약 조건들이 잘 정의되어 있습니다. 다만 몇 가지 고려사항이 있습니다:

  1. member_id에 대한 외래 키 제약 조건이 명시되지 않았습니다
  2. 조회 성능을 위한 인덱스 추가를 고려해보세요 (예: member_id, created_at)
  3. 매장 정보가 비정규화되어 있는데, 이는 성능상 합리적인 선택으로 보입니다

다음과 같은 개선사항을 고려해보세요:

-- 외래 키 제약 조건 추가
ALTER TABLE `story` ADD CONSTRAINT `fk_story_member` 
FOREIGN KEY (`member_id`) REFERENCES `member` (`id`);

-- 성능을 위한 인덱스 추가
CREATE INDEX `idx_story_member_id` ON `story` (`member_id`);
CREATE INDEX `idx_story_created_at` ON `story` (`created_at`);
src/main/java/eatda/exception/BusinessErrorCode.java (1)

45-54: 스토리 관련 오류 코드 메시지의 일관성을 개선하세요.

스토리 관련 오류 코드들이 잘 정의되었지만, 메시지에서 조사 사용이 일관적이지 않습니다.

-    INVALID_STORY_DESCRIPTION("STY001", "스토리 본문은 필수입니다."),
-    INVALID_STORY_IMAGE_URL("STY002", "스토리 이미지 URL은 필수입니다."),
-    STORY_MEMBER_REQUIRED("STY003", "스토리 작성 시 회원 정보는 필수입니다."),
-    STORY_STORE_REQUIRED("STY004", "스토리 작성 시 가게 정보는 필수입니다."),
-    INVALID_STORE_KAKAO_ID("STY007", "스토어 Kakao ID는 필수입니다."),
-    INVALID_STORE_NAME("STY008", "스토어 이름은 필수입니다."),
-    INVALID_STORE_ADDRESS("STY009", "스토어 주소는 필수입니다.");
+    INVALID_STORY_DESCRIPTION("STY001", "스토리 본문은 필수입니다."),
+    INVALID_STORY_IMAGE_URL("STY002", "스토리 이미지 URL은 필수입니다."),
+    STORY_MEMBER_REQUIRED("STY003", "스토리 작성 시 회원 정보는 필수입니다."),
+    STORY_STORE_REQUIRED("STY004", "스토리 작성 시 가게 정보는 필수입니다."),
+    INVALID_STORE_KAKAO_ID("STY007", "스토어 Kakao ID는 필수입니다."),
+    INVALID_STORE_NAME("STY008", "스토어 이름은 필수입니다."),
+    INVALID_STORE_ADDRESS("STY009", "스토어 주소는 필수입니다.");

모든 메시지에서 "~는 필수입니다" 형태로 통일하는 것을 권장합니다.

src/test/java/eatda/document/story/StoryDocumentTest.java (1)

102-134: 이미지 형식 오류 테스트를 개선하세요.

테스트 로직은 적절하지만, 다음 사항들을 개선할 수 있습니다:

  1. 디버깅용 System.out.println 제거
  2. 더 명확한 테스트 데이터 사용
-            System.out.println("응답 상태코드 >>> " + response.statusCode());
-            System.out.println("응답 바디 >>> " + response.asString());

디버깅이 필요한 경우 로깅 프레임워크를 사용하거나, 테스트 완료 후 제거하는 것이 좋습니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d008489 and 510546d.

📒 Files selected for processing (23)
  • src/main/java/eatda/controller/story/FilteredSearchResult.java (1 hunks)
  • src/main/java/eatda/controller/story/StoriesResponse.java (1 hunks)
  • src/main/java/eatda/controller/story/StoryController.java (1 hunks)
  • src/main/java/eatda/controller/story/StoryRegisterRequest.java (1 hunks)
  • src/main/java/eatda/controller/story/StoryResponse.java (1 hunks)
  • src/main/java/eatda/domain/story/Story.java (1 hunks)
  • src/main/java/eatda/exception/BusinessErrorCode.java (2 hunks)
  • src/main/java/eatda/exception/GlobalExceptionHandler.java (3 hunks)
  • src/main/java/eatda/repository/story/StoryRepository.java (1 hunks)
  • src/main/java/eatda/service/common/ImageDomain.java (1 hunks)
  • src/main/java/eatda/service/common/ImageService.java (3 hunks)
  • src/main/java/eatda/service/store/StoreService.java (1 hunks)
  • src/main/java/eatda/service/story/StoryService.java (1 hunks)
  • src/main/resources/db/migration/V3__add_story_table.sql (1 hunks)
  • src/test/java/eatda/controller/BaseControllerTest.java (2 hunks)
  • src/test/java/eatda/controller/story/StoryControllerTest.java (1 hunks)
  • src/test/java/eatda/document/BaseDocumentTest.java (3 hunks)
  • src/test/java/eatda/document/Tag.java (1 hunks)
  • src/test/java/eatda/document/story/StoryDocumentTest.java (1 hunks)
  • src/test/java/eatda/domain/story/StoryTest.java (1 hunks)
  • src/test/java/eatda/service/BaseServiceTest.java (2 hunks)
  • src/test/java/eatda/service/common/ImageServiceTest.java (3 hunks)
  • src/test/java/eatda/service/story/StoryServiceTest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
src/test/java/eatda/controller/BaseControllerTest.java (1)
Learnt from: leegwichan
PR: YAPP-Github/26th-Web-Team-1-BE#60
File: src/test/java/eatda/controller/store/StoreControllerTest.java:10-32
Timestamp: 2025-07-09T07:56:50.612Z
Learning: 컨트롤러 테스트에서 MockitoBean으로 의존성을 모킹한 경우, 상세한 비즈니스 로직 검증보다는 컨트롤러 계층의 동작(라우팅, 파라미터 처리, 응답 구조 등)을 검증하는 것이 더 적절합니다. 모킹된 데이터에 대한 상세 검증은 의미가 없기 때문입니다.
src/main/java/eatda/service/common/ImageService.java (1)
Learnt from: lvalentine6
PR: YAPP-Github/26th-Web-Team-1-BE#68
File: src/main/java/eatda/service/common/ImageService.java:24-24
Timestamp: 2025-07-09T20:11:28.800Z
Learning: ImageService에서 MIME 타입 검증 시 "image/jpg"와 "image/jpeg" 모두 허용해야 함. "image/jpg"는 비표준이지만 실제 환경에서 여전히 사용되어 호환성 문제 방지를 위해 두 타입 모두 지원하는 것이 실용적임.
src/test/java/eatda/controller/story/StoryControllerTest.java (1)
Learnt from: leegwichan
PR: YAPP-Github/26th-Web-Team-1-BE#60
File: src/test/java/eatda/controller/store/StoreControllerTest.java:10-32
Timestamp: 2025-07-09T07:56:50.612Z
Learning: 컨트롤러 테스트에서 MockitoBean으로 의존성을 모킹한 경우, 상세한 비즈니스 로직 검증보다는 컨트롤러 계층의 동작(라우팅, 파라미터 처리, 응답 구조 등)을 검증하는 것이 더 적절합니다. 모킹된 데이터에 대한 상세 검증은 의미가 없기 때문입니다.
src/test/java/eatda/document/BaseDocumentTest.java (1)
Learnt from: leegwichan
PR: YAPP-Github/26th-Web-Team-1-BE#60
File: src/main/java/eatda/controller/store/StoreController.java:18-21
Timestamp: 2025-07-09T08:05:53.497Z
Learning: Spring Boot 컨트롤러에서 LoginMember 등의 인증 파라미터는 메서드 내에서 직접 사용되지 않더라도 접근 제어(인증된 사용자만 접근 가능)를 위해 필요할 수 있다. 이는 보안상 유효한 패턴이다.
src/test/java/eatda/service/common/ImageServiceTest.java (1)
Learnt from: lvalentine6
PR: YAPP-Github/26th-Web-Team-1-BE#68
File: src/main/java/eatda/service/common/ImageService.java:24-24
Timestamp: 2025-07-09T20:11:28.800Z
Learning: ImageService에서 MIME 타입 검증 시 "image/jpg"와 "image/jpeg" 모두 허용해야 함. "image/jpg"는 비표준이지만 실제 환경에서 여전히 사용되어 호환성 문제 방지를 위해 두 타입 모두 지원하는 것이 실용적임.
🧬 Code Graph Analysis (2)
src/test/java/eatda/document/story/StoryDocumentTest.java (2)
src/test/java/eatda/document/RestDocsRequest.java (1)
  • RestDocsRequest (18-75)
src/test/java/eatda/document/RestDocsResponse.java (1)
  • RestDocsResponse (14-40)
src/main/java/eatda/controller/story/StoryController.java (1)
src/main/java/eatda/controller/web/auth/AuthMemberArgumentResolver.java (1)
  • RequiredArgsConstructor (14-36)
🔇 Additional comments (35)
src/main/java/eatda/service/common/ImageDomain.java (1)

1-15: 잘 구현된 이미지 도메인 enum입니다.

타입 안전성을 제공하고 Lombok 어노테이션을 적절히 활용한 깔끔한 구현입니다. 새로운 STORY 도메인이 추가되어 스토리 기능을 잘 지원하고 있습니다.

src/main/java/eatda/controller/story/FilteredSearchResult.java (1)

3-9: 매장 검색 결과를 위한 적절한 record 구조입니다.

필드명이 명확하고 매장 정보를 담기에 적절한 구조로 설계되었습니다. 응답 DTO로서의 역할을 잘 수행할 것으로 보입니다.

src/test/java/eatda/document/Tag.java (1)

7-8: 스토리 API 문서화를 위한 적절한 태그 추가입니다.

새로운 STORY_API 태그가 기존 패턴에 맞게 잘 추가되었으며, API 문서화 분류에 도움이 될 것입니다.

src/test/java/eatda/service/BaseServiceTest.java (2)

8-10: 스토리 기능 테스트를 위한 적절한 import 추가입니다.

새로운 스토리 기능 테스트에 필요한 의존성들이 올바르게 import되었습니다.


32-39: 스토리 관련 테스트를 위한 Mock 빈 설정이 잘 구성되었습니다.

StoreService, ImageService, StoryRepository의 MockitoBean 설정이 적절하게 추가되어 스토리 기능 테스트를 위한 기반을 제공합니다.

src/test/java/eatda/controller/BaseControllerTest.java (1)

59-63: 테스트 인프라 확장 승인

새로운 스토리 기능을 위한 StoryServiceImageService 모킹이 적절하게 추가되었습니다. 기존 패턴과 일치하며, 컨트롤러 테스트에서 필요한 의존성을 효과적으로 모킹할 수 있습니다.

src/main/java/eatda/controller/story/StoriesResponse.java (1)

5-13: 깔끔한 DTO 레코드 구현

StoriesResponse와 중첩된 StoryPreview 레코드가 잘 설계되었습니다. 불변성을 보장하고 명확한 네이밍으로 스토리 미리보기 데이터를 효과적으로 전달할 수 있습니다.

src/main/java/eatda/exception/GlobalExceptionHandler.java (3)

5-5: 로깅 기능 추가 승인

@Slf4j 어노테이션 추가로 예외 처리에 로깅 기능이 도입되었습니다.


82-82: 적절한 비즈니스 예외 로깅

BusinessException 처리 시 에러 코드를 로깅하는 것이 디버깅과 모니터링에 도움이 됩니다.


90-90: 포괄적인 예외 로깅

일반 예외 처리 시 예외 클래스명, 메시지, 스택 트레이스를 모두 로깅하여 문제 진단에 충분한 정보를 제공합니다.

src/test/java/eatda/controller/story/StoryControllerTest.java (2)

18-27: 적절한 컨트롤러 테스트 모킹

imageServicestoryService의 모킹이 컨트롤러 레이어 테스트에 적합하게 설정되었습니다. 고정된 응답값과 동작을 제공하여 컨트롤러의 라우팅과 요청 처리에 집중할 수 있습니다.


32-54: 멀티파트 요청 테스트 잘 구현됨

스토리 등록 API의 멀티파트 요청 처리가 적절하게 테스트되었습니다. JSON 요청 데이터와 이미지 파일을 함께 전송하고 201 상태 코드로 성공을 확인하는 것이 컨트롤러 레이어 테스트에 적합합니다.

src/main/java/eatda/controller/story/StoryResponse.java (1)

3-11: 깔끔한 DTO 구현입니다.

Java record를 사용한 불변 데이터 전송 객체로 적절히 구현되었습니다. 필드명도 명확하고 스토리 응답에 필요한 정보를 잘 담고 있습니다.

src/main/java/eatda/service/common/ImageService.java (4)

23-23: 상수 추출로 가독성이 향상되었습니다.

하드코딩된 "bin" 문자열을 DEFAULT_CONTENT_TYPE 상수로 추출하여 유지보수성이 개선되었습니다.


41-41: 타입 안전성이 향상되었습니다.

String 매개변수를 ImageDomain 열거형으로 변경하여 컴파일 타임에 유효한 도메인만 전달할 수 있도록 개선되었습니다.


45-45: 도메인 기반 키 생성이 적절합니다.

domain.getName()을 사용하여 S3 키를 생성하는 것이 기존 문자열 방식보다 안전하고 일관성 있습니다.


70-70: 일관성 있는 상수 사용.

DEFAULT_CONTENT_TYPE 상수를 사용하여 코드 일관성이 향상되었습니다.

src/main/java/eatda/service/story/StoryService.java (1)

30-48: 트랜잭션 범위와 전반적인 플로우가 적절합니다.

스토리 등록 로직이 올바른 순서로 구현되어 있습니다: 사용자 검증 → 매장 검증 → 이미지 업로드 → 엔티티 생성 → 저장. @Transactional 애노테이션도 적절히 적용되었습니다.

src/main/java/eatda/controller/story/StoryController.java (1)

13-28: REST 컨트롤러 구조가 적절합니다.

멀티파트 데이터 처리와 인증된 사용자 정보 사용이 올바르게 구현되었습니다. HTTP 201 Created 상태 코드도 리소스 생성에 적합합니다.

src/test/java/eatda/service/common/ImageServiceTest.java (3)

20-21: 파라미터화된 테스트 도입이 우수합니다.

@ParameterizedTest@EnumSource를 활용하여 모든 ImageDomain에 대한 테스트 커버리지를 개선했습니다.


54-78: ImageDomain 열거형 활용으로 테스트 코드가 개선되었습니다.

하드코딩된 문자열 대신 ImageDomain 열거형을 사용하여 타입 안전성과 유지보수성이 향상되었습니다. 파라미터화된 테스트로 모든 도메인에 대한 일관된 테스트가 가능해졌습니다.


86-90: ImageDomain.STORY 상수 사용이 적절합니다.

하드코딩된 문자열 대신 열거형 상수를 사용하여 코드의 일관성이 향상되었습니다.

src/test/java/eatda/domain/story/StoryTest.java (1)

1-160: 도메인 검증 테스트가 포괄적이고 잘 구성되어 있습니다.

스토리 도메인의 모든 검증 시나리오를 체계적으로 테스트하고 있습니다:

  • 중첩 클래스로 기능별 테스트 그룹화
  • 성공 케이스와 실패 케이스 모두 포함
  • 모든 필수 필드에 대한 검증 테스트
  • 적절한 에러 메시지 검증
  • Mockito를 활용한 의존성 모킹

도메인 불변성이 올바르게 강제되고 있음을 보장하는 우수한 테스트 코드입니다.

src/test/java/eatda/document/BaseDocumentTest.java (3)

9-15: 새로운 서비스 의존성 추가가 적절합니다.

스토리 기능을 위한 StoryServiceImageService 의존성이 올바르게 추가되었습니다. 관련 import 문도 적절히 포함되었습니다.


51-56: MockitoBean 설정이 올바르게 구성되었습니다.

새로운 서비스들에 대한 모킹 설정이 기존 패턴과 일관성 있게 추가되었습니다.


104-106: EtcErrorCode 지원 메소드 추가가 일관성 있습니다.

기존 BusinessErrorCode와 동일한 패턴으로 EtcErrorCode를 지원하는 메소드가 추가되어 API 문서화의 일관성이 향상되었습니다.

src/test/java/eatda/service/story/StoryServiceTest.java (2)

54-67: 실패 시나리오 테스트가 적절합니다.

매장을 찾을 수 없는 경우에 대한 예외 처리가 올바르게 테스트되고 있습니다. 에러 메시지 검증도 적절히 수행되고 있습니다.


21-31: 테스트 구조와 설정이 잘 구성되어 있습니다.

BaseServiceTest를 상속받아 공통 설정을 재사용하고, 의존성 주입을 통한 서비스 초기화가 적절히 구현되었습니다.

src/main/java/eatda/exception/BusinessErrorCode.java (1)

24-24: 새로운 스토어 오류 코드 추가가 적절합니다.

STORE_NOT_FOUND 오류 코드가 기존 스토어 오류 코드 패턴과 일치하며, 메시지도 명확합니다.

src/main/java/eatda/domain/story/Story.java (4)

21-25: 엔티티 구조가 잘 설계되었습니다.

JPA 엔티티 어노테이션과 Lombok 어노테이션이 적절히 사용되었으며, AuditingEntity 상속으로 생성/수정 시간 관리가 자동화되었습니다.


31-33: Member와의 연관관계 설정이 적절합니다.

지연 로딩 설정으로 성능이 최적화되었고, nullable = false로 필수 관계가 명확히 정의되었습니다.


53-74: 빌더 패턴과 유효성 검사 로직이 우수합니다.

생성자에서 즉시 유효성 검사를 수행하고, 검사 로직이 논리적으로 그룹화되어 있어 가독성이 좋습니다.


76-128: 유효성 검사 메서드들이 잘 분리되어 있습니다.

각 필드별로 private 메서드로 분리된 유효성 검사 로직이 가독성과 유지보수성을 향상시킵니다. BusinessException 사용으로 일관된 예외 처리가 구현되었습니다.

src/test/java/eatda/document/story/StoryDocumentTest.java (2)

29-37: REST Docs 요청 문서화 설정이 적절합니다.

Tag.STORY_API를 사용한 태그 분류와 명확한 summary/description 설정이 잘 되어 있습니다.


39-71: 성공 테스트 케이스가 잘 구현되었습니다.

적절한 모킹 설정과 multipart 요청 구성, 응답 상태 코드 검증이 포함되어 있습니다.

@github-actions
Copy link

📄 Terraform Plan Summary

🛡️ Common Infrastructure


No plan summary

Status: ✅ No Changes


🛠️ Development Environment


No plan summary

Status: ✅ No Changes


📋 Full Results: View in Actions

@github-actions
Copy link

📌 최신 ERD가 자동 생성되었습니다.

👉 ERD 보러가기

@YAPP-Github YAPP-Github deleted a comment from github-actions bot Jul 16, 2025
@YAPP-Github YAPP-Github deleted a comment from github-actions bot Jul 16, 2025
@YAPP-Github YAPP-Github deleted a comment from github-actions bot Jul 16, 2025
@YAPP-Github YAPP-Github deleted a comment from github-actions bot Jul 16, 2025
Copy link
Member

@leegwichan leegwichan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/noti 승로님 고생 많으셨어요! 처음으로 Service, Controller, Document 테스트를 작성하기도 하니 저와 다른 컨텍스트로 진행되는 부분이 있는 것 같아요. 관련해서 리뷰 남겼습니다.
일단 개발 작업이 우선이니 넘어가셔도 문제 없을 것 같습니다. 빠르게 작업 마무리하고 2차 스프린트 들어가기 전에 리팩토링 한 번 하는걸로 하시죠!

Comment on lines +19 to +27
@PostMapping("/api/stories")
public ResponseEntity<Void> registerStory(
@RequestPart("request") StoryRegisterRequest request,
@RequestPart("image") MultipartFile image,
LoginMember member
) {
storyService.registerStory(request, image, member.id());
return ResponseEntity.status(HttpStatus.CREATED).build();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안] POST 요청에서 StoryResponse를 응답하는 건 어떨까요?

  • POST 요청에서 어떻게 만들어졌는지 Response를 반환하는 게 일반적이라고 생각했어요.
  • 클라이언트가 해당 값을 사용하지 않을 거라면, (실용적인 입장에서) 추가하지 않아도 된다고 생각합니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 생각해보니 스토리 등록후에 어디로 리다이렉트 될지 명확하지 않은 상황이네요.
저는 스토리 생성 후 스토리 목록으로 돌아간다고 생각해서 바디를 넣지는 않았는데
이 부분은 프론트팀에 물어보고 필요하다면 수정할께요 :)

Comment on lines +82 to +87
private void validateStore(String storeKakaoId, String storeName, String storeAddress, String storeCategory) {
validateStoreKakaoId(storeKakaoId);
validateStoreName(storeName);
validateStoreAddress(storeAddress);
validateStoreCategory(storeCategory);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[검토] 외부 API를 통해 받는 값들은 검증을 하면 안된다고 생각하는데, Kakao API 문서를 참고하셨다면 크게 문제는 없다고 생각합니다. 혹시 모르니 한 번 더 확인 부탁드릴께요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오.. Kakao API 문서를 참고해서 작성하고 연동 테스트를 거쳤습니다!

외부 API에서 온 값들은 검증을 하지 않아야 한다는 이유가 무엇일까요?
저는 외부에서 들어오는 값이기 때문에 더 검증이 필요하다고 생각했는데요
외부 API 응답에 문제가 있거나 구조가 변경되는 상황을 고려하면,
도메인 로직이 그 영향에서 최대한 독립적이어야 하지 않을까 해서요.

만약 검증을 하지 않는다면
저희 서비스가 외부 API에 완전 종속적이고 장애에 그대로 영향을 받기에 최소한의 방어를 하고 싶었어요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

외부에서 들어오는 값이기 때문에 더 검증이 필요하다고 생각했는데요

저도 외부에서 들어온 값을 검증해야 한다는 것에는 동의합니다. 그런데 "Kakao API 에서 들어온 값이 우리가 정한 도메인 규칙과 일치하지 않아서 가게가 등록되지 않는 상황"에 대한 염려가 큰 것 같아요. 외부 API에서 들어온 모든 정보가 일정한 형식이라는 보장이 되기는 힘드니까요;; 적어도 "특정 형식이 아닐 경우 설정할 기본값"이 있어야 할 것 같아요.
지금은 크게 문제 없을 것 같아 넘어가시죠! 코드 형식적으로 논의해볼 내용들은 제가 한 번 다 정리해서 이야기해보면 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오... 그런 부분도 있겠네요
DTO 계층에서의 유연하게 방어할수 있는 기본값 설정을 하면 딱 좋을것 같네요 😄
2차 스프린트 이후에 리펙토링 시기에 고민해보시죠!

Comment on lines +44 to +45
@Column(name = "store_category", nullable = false)
private String storeCategory;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

category 는 StoreCategory 값이 들어가야 할 것 같은데요. 해당 부분은 Kakao API의 응답값에 따라 Enum으로 바꿔주는 작업이 필요합니다.
일단 넘어가신다면 제가 응원 등록 API 만들면서 같이 작업하도록 하겠습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아! 이전에 만들어둔 StoreCategory enum이 있었네요
일단은 Kakao API 응답값 그대로 사용하는 쪽으로 진행했는데,
응원 등록 쪽 작업하시면서 해당 enum으로 매핑하신다면 좋을 것 같습니다.
필요하시면 이후 작업에도 같이 맞춰갈게요!

Comment on lines +32 to +43
@Test
void 스토리를_등록할_수_있다() {
String requestJson = """
{
"query": "농민백암순대",
"storeKakaoId": "123",
"description": "여기 진짜 맛있어요!"
}
""";

byte[] imageBytes = "dummy image content".getBytes(StandardCharsets.UTF_8);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트 작성하시느라 고생 많으셨어요!
이 부분에 대해서는 제가 추후에 개선안을 찾아볼께요!

Comment on lines 31 to 39

@MockitoBean
protected StoreService storeService;

@MockitoBean
protected ImageService imageService;

@MockitoBean
protected StoryRepository storyRepository;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[수정 필요] ServiceTest 같은 경우에는 H2 DB를 이용하여 테스트를 진행하는데요. 수정이 필요할 것 같습니다.

  • ImageService 만 Mock으로 이용하고 나머지는 실제 객체로 테스트 해주시면 좋을 것 같아요!
  • ServiceTest 관련해서 궁금한 부분 있으시면 편하게 말씀 주세요! (바쁘시다면 문서로 정리해서 남겨놓도록 할께요!)
  • 다른 작업이 있어 바쁘시다면 넘어가셔도 좋습니다. 제가 개선하도록 할께요~!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 StoryRepository 부분을 제가 Mock으로 사용했군요
이 부분은 바로 수정할께요!

그런데 StoreService는 Kakao API로 요청이 발생하게 되는데
이전에 StoreServiceTest, StoreSearchFilterTest에서도 Mockito.doReturn 으로 테스트 하셨는데
어디서 실제 객체로 사용해야 한다고 생각하시는건지 궁금합니다.
말씀해주시면 제가 수정하겠습니다~

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서비스 테스트에서는 기본적으로 **"되도록 모든 객체를 실제 객체를 사용하고, DB만 H2로 사용한다"**는 생각을 하고 있어요. 그런데 테스트를 할 때마다 외부 API에 요청이 가면 안되니까 **"외부 API를 사용하는 최소한의 단위 객체만 Mock을 사용"**하도록 노력하고 있습니다.

현재 작업 환경에서는 외부 API를 호출하는 부분이 MapClient, OauthClient, ImageService 이어서 해당 부분만 Mock으로 처리하고, 나머지 부분은 실 객체를 사용하는 것을 생각하고 있습니다.

그런데 StoreService는 Kakao API로 요청이 발생하게 되는데
이전에 StoreServiceTest, StoreSearchFilterTest에서도 Mockito.doReturn 으로 테스트 하셨는데
어디서 실제 객체로 사용해야 한다고 생각하시는건지 궁금합니다.

이건 제가 잘못한 것 같네요;; 추후에 아래와 같이 수정하겠습니다!

  • ServiceTest 환경에서는 이미 MapClient는 Mock 객체가 빈으로 등록되어 있다.
  • StoreService를 테스트 할 때는 ServiceTest 환경에서 등록된 빈으로 테스트를 진행한다.
   @Autowired
    private StoreService storeService; // Spring Container에 등록된 객체를 사용하여 테스트
    
    @BeforeEach
    void mockingClient() {
        List<StoreSearchResult> searchResults = ...;
        doReturn(searchResults).when(mapClient).searchShops(anyString()); // BaseServiceTest 에 mapClient를 Mock 객체로 등록함
    }

    @Nested
    class SearchStores {

        @Test
        void 음식점_검색_결과를_반환한다() {
            String query = "농민백암순대";

            var response = storeService.searchStores(query);

            assertAll(
                    () -> assertThat(response.stores()).hasSize(2),
                    () -> assertThat(response.stores().get(0).kakaoId()).isEqualTo("123"),
                    () -> assertThat(response.stores().get(0).address()).isEqualTo("서울 강남구 대치동 896-33"),
                    () -> assertThat(response.stores().get(1).kakaoId()).isEqualTo("456"),
                    () -> assertThat(response.stores().get(1).address()).isEqualTo("서울 중구 북창동 19-4")
            );
        }
    }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StoreSearchFilter는 제가 서비스로 생각하지 않았습니다. 검색 결과에 대한 필터링 조건을 담은 "도메인 로직"이라고 생각했습니다. 그래서 도메인으로 패키지를 옮길까하다가 하지 않았는데, 오히려 착각을 불러일으킨 것 같네요;;
그래서 따로 BaseServiceTest를 상속하지 않고 단위 테스트를 진행했습니다.

Copy link
Member Author

@lvalentine6 lvalentine6 Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아~~!
StoreService에 Mapclilent가 실제 카카오 api 호출을 하니 그것만 Mock 처리하고
StoreService 자체는 실객체로 한다는거군요
저도 나눌수 있다면 최소한으로 Mock처리하고 테스트하는게 맞다고 생각해요 😄

StoreSearchFilter는 다시보니 도메인에 가깝긴 하네요 필터링을 하니까요
이제 이해했습니다!

해당 부분 다음 PR에 반영할께요

Comment on lines 53 to 62
@Builder
public Story(
Member member,
String storeKakaoId,
String storeName,
String storeAddress,
String storeCategory,
String description,
String imageKey
) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[제안] 빌더를 사용하고 생성자를 사용하지 않는 상황에서는 해당 접근 제어자를 private로 하는 것을 선호합니다. 이에 대해서 어떻게 생각하시나요?

@github-actions
Copy link

📄 Terraform Plan Summary

🛡️ Common Infrastructure


No plan summary

Status: ✅ No Changes


🛠️ Development Environment


No plan summary

Status: ✅ No Changes


📋 Full Results: View in Actions

@github-actions
Copy link

📌 최신 ERD가 자동 생성되었습니다.

👉 ERD 보러가기

@sonarqubecloud
Copy link

@lvalentine6
Copy link
Member Author

lvalentine6 commented Jul 17, 2025

/noti

리뷰하시느라 고생많으셨습니다!
리뷰 하신 코멘트에 답글 달았으니 확인해주세요~

스토리 목록 조회 기능 개발로 인해 먼저 Merge하겠습니다.

@lvalentine6 lvalentine6 merged commit 453bacf into develop Jul 17, 2025
9 checks passed
@lvalentine6 lvalentine6 deleted the feat/PRODUCT-148 branch July 17, 2025 19:24
@github-actions
Copy link

🎉 This PR is included in version 1.4.0-develop.20 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions
Copy link

🎉 This PR is included in version 1.5.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants